<!doctype html>
<html lang="zh">
<head>
<meta charset="utf-8" />
<title>Minimal Candy Crush</title>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<style>
  :root { --cell: 56px; --gap: 6px; }
  body { font-family: Inter, "Segoe UI", Roboto, "Helvetica Neue", Arial; background:#101216; color:#e6eef8; display:flex; flex-direction:column; align-items:center; padding:18px; }
  h1 { margin:6px 0 14px; font-weight:600; }
  #hud { display:flex; gap:12px; align-items:center; margin-bottom:10px; }
  #boardWrap { background:#0c1116; padding:14px; border-radius:12px; box-shadow: 0 6px 24px rgba(0,0,0,0.6); }
  #board { position:relative; width: calc(var(--cell) * var(--cols) + var(--gap) * (var(--cols) - 1)); height: calc(var(--cell) * var(--rows) + var(--gap) * (var(--rows) - 1)); background: #071018; border-radius:8px; padding:6px; display:block; }
  .cellView {
    position:absolute; width:var(--cell); height:var(--cell); border-radius:8px;
    display:flex; align-items:center; justify-content:center; font-weight:700; color:#fff; user-select:none;
    transition: transform 180ms ease, opacity 200ms linear;
    box-shadow: 0 4px 8px rgba(0,0,0,0.4), inset 0 -4px rgba(0,0,0,0.08);
  }
  .c0 { background: transparent; opacity:0; }
  .c1 { background:#ff6b6b; } /* red */
  .c2 { background:#ffb86b; color:#000; } /* orange */
  .c3 { background:#ffd86b; color:#000; } /* yellow */
  .c4 { background:#6be17c; } /* green */
  .c5 { background:#6bb7ff; } /* blue */
  .c6 { background:#b86bff; } /* purple */

  .crushAnim { animation: crushFade 260ms linear forwards; }
  @keyframes crushFade { to { transform: scale(.6); opacity:0; } }

  #controls { margin-top:10px; display:flex; gap:8px; justify-content:center; }
  button { background:#1b2630; color:#e6eef8; border:0; padding:8px 12px; border-radius:8px; cursor:pointer; }
  .small { font-size:13px; padding:6px 10px; }

  #info { font-size:14px; }
  #msg { margin-top:10px; color:#ffd; min-height:18px; text-align:center; }
  .disabled { opacity:0.6; pointer-events:none; }
</style>
</head>
<body>
  <h1>Minimal Candy Crush</h1>
  <div id="hud">
    <div id="info">Score: <span id="score">0</span> &nbsp;|&nbsp; Moves left: <span id="moves">30</span></div>
    <div>
      <button id="resetBtn">重置</button>
      <button id="autoBtn">自动连锁（ON）</button>
    </div>
  </div>

  <div id="boardWrap">
    <div id="board" style="--rows:8; --cols:8"></div>
  </div>
  <div id="msg"></div>
  <div id="controls">
    <button class="small" id="hintBtn">提示一手</button>
    <button class="small" id="shuffleBtn">洗牌</button>
  </div>

<script>
(() => {
  // 配置
  const ROWS = 8, COLS = 8;
  const TYPES = 6; // 1..TYPES
  const CELL = 56, GAP = 6;
  let autoCascade = true;

  const boardEl = document.getElementById('board');
  const scoreEl = document.getElementById('score');
  const movesEl = document.getElementById('moves');
  const msgEl = document.getElementById('msg');
  const resetBtn = document.getElementById('resetBtn');
  const hintBtn = document.getElementById('hintBtn');
  const shuffleBtn = document.getElementById('shuffleBtn');
  const autoBtn = document.getElementById('autoBtn');

  // 状态
  let board = []; // int[ROWS][COLS]
  let score = 0, moves = 30;
  let locked = false; // 动画期间锁住输入
  let dragStart = null; // {r,c,el}
  let cellEls = {}; // map "r,c" -> DOM element

  // 布局参数
  boardEl.style.setProperty('--rows', ROWS);
  boardEl.style.setProperty('--cols', COLS);

  // 帮助：生成随机糖果（不为0）
  function randCandy() {
    return 1 + Math.floor(Math.random() * TYPES);
  }

  // 创建初始 board，且保证初始无可消（可选）
  function initBoard() {
    board = Array.from({length: ROWS}, () => Array.from({length: COLS}, () => 0));
    // 填充并避免初始匹配
    for (let r=0;r<ROWS;r++){
      for (let c=0;c<COLS;c++){
        do {
          board[r][c] = randCandy();
        } while ((c>=2 && board[r][c]===board[r][c-1]&&board[r][c]===board[r][c-2]) ||
                 (r>=2 && board[r][c]===board[r-1][c]&&board[r][c]===board[r-2][c]));
      }
    }
    score = 0;
    moves = 30;
    updateHud();
    renderAll();
    setMsg('');
  }

  // HUD
  function updateHud(){ scoreEl.textContent = score; movesEl.textContent = moves; }

  // DOM 渲染：我们用绝对定位每个格子，位置基于 r,c
  function renderAll(animate=false) {
    // 清旧元素
    boardEl.innerHTML = '';
    cellEls = {};
    for (let r=0;r<ROWS;r++){
      for (let c=0;c<COLS;c++){
        const v = board[r][c];
        const el = document.createElement('div');
        el.className = 'cellView c' + v;
        if (v === 0) el.classList.add('c0');
        el.setAttribute('data-pos', `${r},${c}`);
        el.style.width = CELL + 'px';
        el.style.height = CELL + 'px';
        const left = c * (CELL + GAP);
        const top = r * (CELL + GAP);
        el.style.transform = `translate(${left}px, ${top}px)`;
        el.style.transition = animate ? 'transform 180ms ease, opacity 200ms linear' : 'none';
        el.textContent = v === 0 ? '' : v;
        el.addEventListener('mousedown', onPointerDown);
        el.addEventListener('touchstart', onPointerDown, {passive:true});
        cellEls[`${r},${c}`] = el;
        boardEl.appendChild(el);
      }
    }
  }

  // 将某个单元刷新显示（值已改 board）
  function refreshCell(r,c) {
    const el = cellEls[`${r},${c}`];
    if (!el) return;
    const v = board[r][c];
    el.className = 'cellView c' + v;
    if (v===0) el.classList.add('c0');
    el.textContent = v===0?'':v;
  }

  // 返回邻接判定
  function isNeighbor(a,b){
    const dr = Math.abs(a.r - b.r), dc = Math.abs(a.c - b.c);
    return (dr===1 && dc===0) || (dr===0 && dc===1);
  }

  // 交换逻辑（交换 board 值），不修改 DOM 位置（render 后会更新）
  function swapBoard(a,b) {
    const tmp = board[a.r][a.c];
    board[a.r][a.c] = board[b.r][b.c];
    board[b.r][b.c] = tmp;
  }

  // 查找所有匹配，返回被消除位置数组 [{r,c}]
  function findMatches() {
    const toCrush = new Set();
    // horizontal
    for (let r=0;r<ROWS;r++){
      let j=0;
      while (j < COLS) {
        const v = board[r][j];
        if (v===0) { j++; continue; }
        let k = j+1;
        while (k<COLS && board[r][k]===v) k++;
        if (k-j >= 3) {
          for (let t=j;t<k;t++) toCrush.add(`${r},${t}`);
        }
        j = k;
      }
    }
    // vertical
    for (let c=0;c<COLS;c++){
      let i=0;
      while (i < ROWS) {
        const v = board[i][c];
        if (v===0) { i++; continue; }
        let k = i+1;
        while (k<ROWS && board[k][c]===v) k++;
        if (k-i >= 3) {
          for (let t=i;t<k;t++) toCrush.add(`${t},${c}`);
        }
        i = k;
      }
    }
    return Array.from(toCrush).map(s => s.split(',').map(Number));
  }

  // 消除：把指定位置设为0并产生得分；返回消除数量
  function doCrush(cells) {
    let gained = 0;
    for (const [r,c] of cells) {
      if (board[r][c] !== 0) {
        gained++;
        board[r][c] = 0;
      }
    }
    // 简单计分：每个格 100 分；连锁可以以后加倍
    score += gained * 100;
    updateHud();
    return gained;
  }

  // 下落补充：按列处理，返回是否有移动（用于连锁判断）
  function doDropAndFill() {
    let moved = false;
    for (let c=0;c<COLS;c++){
      let write = ROWS-1;
      for (let read = ROWS-1; read>=0; read--) {
        if (board[read][c] !== 0) {
          if (read !== write) {
            board[write][c] = board[read][c];
            board[read][c] = 0;
            moved = true;
          } else {
            board[write][c] = board[read][c];
          }
          write--;
        }
      }
      // 填充顶部新糖果
      for (let r=write; r>=0; r--) {
        board[r][c] = randCandy();
        moved = true;
      }
    }
    return moved;
  }

  // 播放一轮：执行消除->下落->连锁。UI 调用此函数并负责节拍（锁定期间禁止操作）
  async function playCascade() {
    // if (!locked) return;
    // locked = true;
    let totalGainedThisAction = 0;
    // 连锁循环
    do {
      const matches = findMatches();
      if (matches.length === 0) break;
      // 标记动画：让对应 DOM 播放缩放淡出
      for (const [r,c] of matches) {
        const el = cellEls[`${r},${c}`];
        if (el) el.classList.add('crushAnim');
      }
      // 等待动画
      await wait(260);
      // 消除并计分
      const gained = doCrush(matches);
      totalGainedThisAction += gained;
      renderAll(true); // 重新渲染新状态（0 会显示为空）
      await wait(80);
      // 下落与补充
      doDropAndFill();
      renderAll(true);
      await wait(220); // 等下落可见
    } while (autoCascade);
    // action end
    locked = false;
    return totalGainedThisAction;
  }

  // 简单等待
  function wait(ms){ return new Promise(res => setTimeout(res, ms)); }

  // 处理玩家发起的交换（swap a<->b）
  async function trySwap(a,b) {
    if (locked) return;
    if (!isNeighbor(a,b)) return;
    locked = true;
    // 做交换并渲染（快速视觉反馈）
    swapBoard(a,b);
    renderAll(true);
    // 等动画短暂显示
    await wait(160);
    // 检查是否产生匹配
    const matches = findMatches();
    if (matches.length === 0) {
      // 回弹：无效移动，换回
      swapBoard(a,b);
      renderAll(true);
      await wait(160);
      setMsg('Invalid move — no match', 1200);
      locked = false;
      return false;
    }
    // 有效移动：消耗一步，播放消除/连锁
    moves = Math.max(0, moves - 1);
    updateHud();
    await playCascade();
    // 判定胜负
    checkEnd();
    locked = false;
    return true;
  }

  // 检查是否胜利或失败（简单规则：达到某分数胜利，步数耗尽失败）
  const TARGET_SCORE = 5000;
  function checkEnd(){
    if (score >= TARGET_SCORE) {
      setMsg('You Win! Score goal reached 🎉');
      lockAll();
    } else if (moves <= 0) {
      setMsg('No moves left — Game Over');
      lockAll();
    }
  }
  function lockAll(){ locked = true; }

  // UI 消息
  let msgTimer = null;
  function setMsg(t, timeout=0) {
    msgEl.textContent = t || '';
    if (msgTimer) { clearTimeout(msgTimer); msgTimer = null; }
    if (timeout>0) msgTimer = setTimeout(()=> setMsg(''), timeout);
  }

  // 鼠标事件：开始拖拽、结束释放并判断目标
  function onPointerDown(e) {
    if (locked) return;
    e.preventDefault();
    const el = e.currentTarget;
    const [r,c] = el.getAttribute('data-pos').split(',').map(Number);
    dragStart = { r, c, el };
    document.addEventListener('mouseup', onPointerUp);
    document.addEventListener('touchend', onPointerUp);
    document.addEventListener('mousemove', onPointerMove);
    document.addEventListener('touchmove', onPointerMove, {passive:false});
  }

  function onPointerMove(e) {
    if (!dragStart) return;
    // optional: we could show drag ghost; keep simple: track last hovered cell
    // prevent page scroll on touch drag
    if (e.cancelable) e.preventDefault();
  }

  async function onPointerUp(e) {
    document.removeEventListener('mouseup', onPointerUp);
    document.removeEventListener('touchend', onPointerUp);
    document.removeEventListener('mousemove', onPointerMove);
    document.removeEventListener('touchmove', onPointerMove);
    if (!dragStart) return;
    // determine target cell by mouse/touch pos
    let clientX, clientY;
    if (e.changedTouches && e.changedTouches[0]) {
      clientX = e.changedTouches[0].clientX; clientY = e.changedTouches[0].clientY;
    } else {
      clientX = e.clientX; clientY = e.clientY;
    }
    // find nearest cell under cursor
    const rect = boardEl.getBoundingClientRect();
    const localX = clientX - rect.left;
    const localY = clientY - rect.top;
    const c = Math.floor(localX / (CELL + GAP));
    const r = Math.floor(localY / (CELL + GAP));
    // clamp
    const r2 = Math.max(0, Math.min(ROWS-1, r));
    const c2 = Math.max(0, Math.min(COLS-1, c));
    const from = { r: dragStart.r, c: dragStart.c };
    const to = { r: r2, c: c2 };
    dragStart = null;
    // 如果点的是自己或者不邻接则不动作
    if (from.r === to.r && from.c === to.c) return;
    if (!isNeighbor(from,to)) {
      setMsg('Must swap with adjacent cell', 900);
      return;
    }
    await trySwap(from,to);
  }

  // 按钮事件
  resetBtn.addEventListener('click', ()=> { locked=false; initBoard(); });
  autoBtn.addEventListener('click', ()=> {
    autoCascade = !autoCascade;
    autoBtn.textContent = '自动连锁（' + (autoCascade ? 'ON' : 'OFF') + '）';
  });
  hintBtn.addEventListener('click', ()=> {
    const h = findHint();
    if (!h) setMsg('No hint found', 1200);
    else {
      setMsg(`Hint: swap (${h.a.r},${h.a.c}) ↔ (${h.b.r},${h.b.c})`, 2200);
    }
  });
  shuffleBtn.addEventListener('click', ()=> {
    shuffleBoard();
    renderAll(true);
  });

  // 简单找提示：遍历所有相邻交换，看哪一格可产生匹配
  function findHint() {
    for (let r=0;r<ROWS;r++){
      for (let c=0;c<COLS;c++){
        const a = {r,c};
        const deltas = [[1,0],[-1,0],[0,1],[0,-1]];
        for (const d of deltas) {
          const b = { r: r+d[0], c: c+d[1] };
          if (b.r<0||b.r>=ROWS||b.c<0||b.c>=COLS) continue;
          swapBoard(a,b);
          const matches = findMatches();
          swapBoard(a,b);
          if (matches.length>0) return {a,b};
        }
      }
    }
    return null;
  }

  // 洗牌：把所有格子打乱（防止无解）
  function shuffleBoard() {
    const arr = [];
    for (let r=0;r<ROWS;r++) for (let c=0;c<COLS;c++) arr.push(board[r][c]);
    for (let i=arr.length-1;i>0;i--) {
      const j = Math.floor(Math.random()*(i+1));
      [arr[i],arr[j]] = [arr[j],arr[i]];
    }
    for (let r=0, idx=0;r<ROWS;r++) for (let c=0;c<COLS;c++) board[r][c] = arr[idx++];
    // 若乱出初始消除，则再shuffle
    if (findMatches().length>0) shuffleBoard();
  }

  // 初始化
  initBoard();
})();
</script>
</body>
</html>
